/**
 * File ImageConversionWorker.java
 * ---------------------------------------------------------
 *
 * Copyright (C) 2012 David Bauske (david.bauske@googlemail.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * - The origin of the software must not be misrepresented.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * TL;DR: As long as you clearly give me credit for this Software, you are free to use as you like, even in commercial software, but don't blame me
 * 		if it breaks something.
 */
package de.erb.erir.model;

import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.imageio.ImageIO;

import net.cl.util.files.FileUtils;
import net.cl.util.images.ImageUtils;
import net.cl.util.patterns.ProgressListener;
import net.cl.util.patterns.Provider;
import net.cl.util.values.PercentOrAbsoluteInteger;
import de.erb.erir.model.ConversionProgressEvent.Status;

/**
 * Runnable worker class that is capable of reading a list of images and rescaling them according to input arguments.
 *
 * @author David
 */
public class ImageConversionWorker extends
		Provider<ProgressListener<ConversionProgressEvent>>
		implements Runnable
{
	/**
	 * Contains all supported file extensions. All extensions are lowercase. The set is immutable, so all modifying
	 * calls will result in an UnsupportedOperationException.
	 */
	public static final Set<String>		SUPPORTED_EXTENSIONS;
	static
	{
		Set<String> extensions = new HashSet<String>();
		extensions.add("jpg");
		extensions.add("jpeg");
		extensions.add("png");
		extensions.add("gif");
		extensions.add("bmp");
		SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
	}

	/**
	 * Interface for all utilities that can make an output file from an input file.
	 *
	 * @author David
	 */
	public interface OutputFileNameCreator
	{
		public File getOutputFile(File pInput, int pTotalFileCount);
	}

	private List<File>					mFinalFileList;
	private PercentOrAbsoluteInteger	mDestWidth;
	private PercentOrAbsoluteInteger	mDestHeight;
	private OutputFileNameCreator		mOutputFileNameCreator;

	/**
	 * Constructs a new ImageConversionWorker.
	 *
	 * At least one of the two arguments pDestWidth and pDestHeight must not be null.
	 *
	 * @param pFiles The list of files to convert. The class assumes that the list only contains 'real' files (thus no
	 *            directories or similar).
	 * @param pDestWidth The target width of the images. Must be non-null if pDestHeight is null.
	 * @param pDestHeight The target height of the images. Must be non-null if pDestWidth is null.
	 */
	public ImageConversionWorker(List<File> pFiles, PercentOrAbsoluteInteger pDestWidth,
			PercentOrAbsoluteInteger pDestHeight)
	{
		mFinalFileList = pFiles;
		mDestWidth = pDestWidth;
		mDestHeight = pDestHeight;
		mOutputFileNameCreator = new DefaultOutputFileNameCreator("scaled/", "_scaled");
	}

	/**
	 * Constructs a new ImageConversionWorker.
	 *
	 * Similar to {@link #ImageConversionWorker(List, PercentOrAbsoluteInteger, PercentOrAbsoluteInteger)}, but lets you
	 * specify your own OutputFileNameCreator.
	 *
	 * @param pFileNameCreator The object to use for creating output file names
	 * @throws NullPointerException if pFileNameCreator is null.
	 */
	public ImageConversionWorker(List<File> pFiles, PercentOrAbsoluteInteger pDestWidth,
			PercentOrAbsoluteInteger pDestHeight, OutputFileNameCreator pFileNameCreator)
	{
		if(pFileNameCreator == null)
		{
			throw new NullPointerException("pFileNameCreator may not be null");
		}

		mFinalFileList = pFiles;
		mDestWidth = pDestWidth;
		mDestHeight = pDestHeight;
		mOutputFileNameCreator = pFileNameCreator;
	}

	@Override
	public void run()
	{
		boolean listenersNotified = false;

		try
		{
			int convertedCount = 0;
			int curFile = 0;

			for(File file : mFinalFileList)
			{
				if(!file.isFile())
				{
					// Cannot happen since the collectFiles() method only adds 'real' files.
					throw new AssertionError();
				}

				if(Thread.currentThread().isInterrupted())
				{
					// External interruption -> end converting
					for(ProgressListener<ConversionProgressEvent> listener : mListeners)
					{
						listener.onOperationCancelled(new ConversionProgressEvent(
								this, 0.0, ConversionProgressEvent.Status.CANCELLED));
					}
					listenersNotified = true;
					return;
				}

				++curFile;
				double progress = ((double) curFile) / mFinalFileList.size();
				for(ProgressListener<ConversionProgressEvent> listener : mListeners)
				{
					listener.onProgress(new ConversionProgressEvent(this,
							progress));
				}

				try
				{
					String fileExt = FileUtils.getFileExtension(file);

					if(SUPPORTED_EXTENSIONS.contains(fileExt.toLowerCase()))
					{
						BufferedImage srcImage = ImageIO.read(file);
						if(srcImage != null)
						{
							// Image could be loaded successfully. (read() returns null if file is not an image)

							int finalWidth;
							if(mDestWidth != null)
							{
								finalWidth = mDestWidth.apply(srcImage.getWidth());
							}
							else
							{
								// Height was entered by the user, but width was not. Scale accordingly.
								double scaleFactor = ((double) mDestHeight.getValue())
										/ srcImage.getHeight();
								finalWidth = (int) (srcImage.getWidth() * scaleFactor);
							}

							int finalHeight;
							if(mDestHeight != null)
							{
								finalHeight = mDestHeight.apply(srcImage.getHeight());
							}
							else
							{
								// Width was entered by the user, but height was not. Scale accordingly.
								double scaleFactor = ((double) mDestWidth.getValue())
										/ srcImage.getWidth();
								finalHeight = (int) (srcImage.getHeight() * scaleFactor);
							}

							if(finalWidth != srcImage.getWidth()
									|| finalHeight != srcImage.getHeight())
							{
								// Image needs to be scaled
								BufferedImage destImage = ImageUtils.getScaledImage(srcImage,
										finalWidth,
										finalHeight,
										RenderingHints.VALUE_INTERPOLATION_BICUBIC, true);

								File destFile = mOutputFileNameCreator.getOutputFile(file,
										mFinalFileList.size());

								ImageIO.write(destImage, fileExt, destFile);

								++convertedCount;
							}
						}
					}
				}
				catch(IOException e)
				{
					// Nothing we can do about here. Notify listeners and die.
					e.printStackTrace();
					for(ProgressListener<ConversionProgressEvent> listener : mListeners)
					{
						listener.onOperationCancelled(new ConversionProgressEvent(
								this, 0.0, Status.IO_ERROR, e.getMessage()));
					}
					listenersNotified = true;
					return;
				}
			}

			// Signal the successful operation
			Status status = convertedCount > 0 ? Status.DONE_FILES_CONVERTED : Status.DONE_NO_CENVERTIBLE_FILES;
			for(ProgressListener<ConversionProgressEvent> listener : mListeners)
			{
				listener.onOperationDone(new ConversionProgressEvent(this, 1.0, status));
			}
			listenersNotified = true;
		}
		finally
		{
			if(!listenersNotified)
			{
				// Signal the failed operation
				for(ProgressListener<ConversionProgressEvent> listener : mListeners)
				{
					listener.onOperationCancelled(new ConversionProgressEvent(
							this, 0.0, Status.UNKNOWN_ERROR));
				}
			}
		}
	}
}
